﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace GameComponents
{
    public enum EmissionType { Default = 0, EnergyChargge, Explosive, Fireball, Sifter }

    public class ParticleEmitter
    {
        const int MAX_PRIMITIVES = 65535;

        private Random chaos = new Random();
        private GraphicsDevice graphicsDevice;
        private VertexBuffer vertexBuffer;
        private IndexBuffer indexBuffer;
        private DynamicVertexBuffer instanceBuffer;
        private Effect particleEffect;
        private int primitivesPerMember;
        
        private Matrix[] aryWorlds;
        private Color[] aryColors;
        private Particle[] aryParticles;
        private int activeParticles;
        private List<ActiveQueue> lstQueues = new List<ActiveQueue>(10);

        private Vector3 emitPosition = Vector3.Zero;
        private Vector3 emitForward = Vector3.Forward;
        private Vector3 emitUp = Vector3.Up;

        public int BufferSize = 1;
        public float Delay = .05f;
        public float Intensity = 25;

        public ParticleEmitter(GraphicsDevice newGraphicsDevice, Model newParticleModel, Effect newEffect, int numParticles)
        {
            graphicsDevice = newGraphicsDevice;
            particleEffect = newEffect;
            vertexBuffer = newParticleModel.Meshes[0].MeshParts[0].VertexBuffer;
            indexBuffer = newParticleModel.Meshes[0].MeshParts[0].IndexBuffer;
            instanceBuffer = new DynamicVertexBuffer(graphicsDevice, InstanceVD, numParticles, BufferUsage.WriteOnly);
            primitivesPerMember = (int)(indexBuffer.IndexCount / 3);


            activeParticles = 0;

            aryParticles = new Particle[numParticles];
            aryWorlds = new Matrix[numParticles];
            aryColors = new Color[numParticles];
            for (int i = 0; i < numParticles; i++)
            {
                aryParticles[i] = new Particle(emitPosition, emitForward, emitUp);
                aryWorlds[i] = Matrix.Identity;
                aryColors[i] = Color.White;
            }
        }

        // ***** ACCESSORS *****

        public Vector3 Position { get { return emitPosition; } set { emitPosition = value; } }
        public Vector3 Forward { get { return emitForward; } set { emitForward = value; } }
        public Vector3 Up { get { return emitUp; } set { emitUp = value; } }

        // ***** USER METHODS *****

        public void Activate(EmissionType newEmitType, float newTimeSpan, Vector3 newPos, float newSize, float newIntensity, float newDelay)
        {
            lstQueues.Add(new ActiveQueue(newEmitType, newTimeSpan, newPos, newSize, newIntensity, newDelay));

            if (lstQueues.Count > 500)
            { throw new InvalidOperationException("Error: Max Particle Queues"); }
        }

        public void Draw()
        {
            if (activeParticles > 0)
            {
                RefreshData();

                // Set device
                graphicsDevice.SetVertexBuffers(null);
                graphicsDevice.Indices = null;
                graphicsDevice.SetVertexBuffers(new VertexBufferBinding(vertexBuffer, 0, 0), new VertexBufferBinding(instanceBuffer, 0, 1));
                graphicsDevice.Indices = indexBuffer;

                // Draw
                foreach (EffectPass passes in particleEffect.CurrentTechnique.Passes)
                {
                    passes.Apply();
                    graphicsDevice.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, vertexBuffer.VertexCount, 0, primitivesPerMember, activeParticles);
                }
            }
        }

        public void Emit(EmissionType newEmitType, float newTimeSpan, Vector3 newPos, float newSize, float newIntensity)
        {
            switch (newEmitType)
            {
                case EmissionType.Sifter:
                    // if particle available
                    if (activeParticles < aryParticles.Length)
                    {
                        int energyCreated = 0;
                        Vector3 pDirection;
                        while ((activeParticles < aryParticles.Length - BufferSize) && (energyCreated < 1))
                        {
                            Particle particleHold = aryParticles[activeParticles];
                            pDirection = Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble()));
                            particleHold.Emit(newPos + newIntensity * pDirection, -pDirection, emitUp, newTimeSpan, 0, newSize, newIntensity);
                            aryParticles[activeParticles] = particleHold;
                            activeParticles++;
                            energyCreated++;
                        }
                    }
                    break;
            }
        }

        public void RefreshData()
        {
            for (int i = 0; i < activeParticles; i++)
            {
                aryWorlds[i] = aryParticles[i].World;
                aryColors[i] = aryParticles[i].PColor * aryParticles[i].LifePercent;
            }

            instanceBuffer.SetData(0, aryWorlds, 0, activeParticles, InstanceVD.VertexStride, SetDataOptions.Discard);
            instanceBuffer.SetData(64, aryColors, 0, activeParticles, InstanceVD.VertexStride, SetDataOptions.Discard);
        }

        public void Update(float tSeconds)
        {
            // Emit for Queues
            for (int i = 0; i < lstQueues.Count; i++)
            {
                ActiveQueue queueHold = lstQueues[i];
                if (queueHold.TimeSpan > 0)
                {
                    if (queueHold.CurrentDelay > 0)
                    { 
                        queueHold.CurrentDelay -= tSeconds;
                    }
                    else
                    {
                        switch (queueHold.EmitType)
                        {
                            case EmissionType.Default:
                                // if particle available
                                if (activeParticles < aryParticles.Length - 1)
                                {
                                    Particle particleHold = aryParticles[activeParticles];
                                    particleHold.Emit(queueHold.Position, Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble())),
                                        emitUp, 1, Intensity, .25f, .5f * Intensity);
                                    
                                    aryParticles[activeParticles] = particleHold;
                                    activeParticles++;
                                }
                                break;
                            case EmissionType.EnergyChargge:
                                // if particle available
                                if (activeParticles < aryParticles.Length - 1)
                                {
                                    int energyCreated = 0;
                                    Vector3 pDirection;
                                    while ((activeParticles < aryParticles.Length - 1) && (energyCreated < 10))
                                    {
                                        Particle particleHold = aryParticles[activeParticles];
                                        pDirection = Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble()));
                                        particleHold.Emit(queueHold.Position + queueHold.Intensity * pDirection, -pDirection, emitUp, queueHold.Delay, 0, queueHold.Size, 0);
                                        aryParticles[activeParticles] = particleHold;
                                        activeParticles++;
                                        energyCreated++;
                                    }
                                }
                                break;
                            case EmissionType.Explosive:
                                // if particlesf available
                                int particlesCreated = 0;
                                while ((activeParticles < aryParticles.Length - 1) && (particlesCreated < 2))
                                {
                                    Particle particleHold = aryParticles[activeParticles];
                                    
                                    particleHold.Emit(queueHold.Position, Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - .5f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble())),
                                        emitUp, 1, Intensity, .25f, .5f * Intensity);

                                    aryParticles[activeParticles] = particleHold;
                                    activeParticles++;
                                    particlesCreated++;
                                }
                                break;
                            case EmissionType.Fireball:
                                // if particle available
                                if (activeParticles < aryParticles.Length - 1)
                                {
                                    int fireBallsCreated = 0;
                                    Vector3 pDirection;
                                    while ((activeParticles < aryParticles.Length - 1) && (fireBallsCreated < 3))
                                    {
                                        Particle particleHold = aryParticles[activeParticles];
                                        pDirection = Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble()));
                                        particleHold.Emit(queueHold.Position + .2f * pDirection, pDirection, emitUp, queueHold.Delay, 0, .5f, 0);
                                        aryParticles[activeParticles] = particleHold;
                                        activeParticles++;
                                        fireBallsCreated++;
                                    }

                                    fireBallsCreated = 0;
                                    while ((activeParticles < aryParticles.Length - 1) && (fireBallsCreated < 8))
                                    {
                                        Particle fireHold = aryParticles[activeParticles];
                                        pDirection = Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble()));
                                        fireHold.Emit(queueHold.Position + .5f * pDirection, pDirection, emitUp, queueHold.Delay, queueHold.Intensity, .125f, 0);

                                        aryParticles[activeParticles] = fireHold;
                                        activeParticles++;
                                        fireBallsCreated++;
                                    }
                                }
                                break;
                            case EmissionType.Sifter:
                                // if particle available
                                if (activeParticles < aryParticles.Length)
                                {
                                    int energyCreated = 0;
                                    Vector3 pDirection;
                                    while ((activeParticles < aryParticles.Length) && (energyCreated < 1))
                                    {
                                        Particle particleHold = aryParticles[activeParticles];
                                        pDirection = Vector3.Normalize(new Vector3(1.0f - 2.0f * (float)chaos.NextDouble(), 2.0f * (float)chaos.NextDouble(), 1.0f - 2.0f * (float)chaos.NextDouble()));
                                        particleHold.Emit(queueHold.Position + queueHold.Intensity * pDirection, -pDirection, emitUp, queueHold.TimeSpan, 0, queueHold.Size, Intensity);
                                        aryParticles[activeParticles] = particleHold;
                                        activeParticles++;
                                        energyCreated++;
                                    }
                                }
                                break;
                        }
                        queueHold.CurrentDelay = queueHold.Delay;
                    }
                    queueHold.TimeSpan -= tSeconds;
                    lstQueues[i] = queueHold;
                }
                // else remove from list
                else
                {
                    lstQueues.RemoveAt(i);
                    i--;
                }
            }

            // Update active particles
            for (int i = 0; i < activeParticles; i++)
            {
                Particle particleHold = aryParticles[i];
                if (particleHold.Update(tSeconds))
                {
                    aryParticles[i] = particleHold;
                }
                // else remove from active
                else
                {
                    aryParticles[i] = aryParticles[activeParticles - 1];
                    activeParticles--;
                    i--;
                }
            }
        }

        // ***** EXTENDED MEMBERS *****

        public struct ActiveQueue
        {
            public EmissionType EmitType;
            public float TimeSpan;
            public Vector3 Position;

            public float Size;
            public float Intensity;
            public float Delay;
            public float CurrentDelay;

            public ActiveQueue(EmissionType newType, float newTimeSpan, Vector3 newPosition, float newSize, float newIntensity, float newDelay)
            { EmitType = newType; TimeSpan = newTimeSpan; Position = newPosition; Size = newSize; Intensity = newIntensity; Delay = newDelay; CurrentDelay = 0; }
        }

        public struct Particle
        {
            public Vector3 Position;
            public Vector3 Forward;
            public Vector3 Up;
            private float initialLife;
            private float lifeSpan;

            public Color PColor;
            public float Speed;
            public float Size;
            public float Gravity;
            private float currentGravity;

            public Particle(Vector3 newPos, Vector3 newForward, Vector3 newUp)
            {
                Position = newPos;
                Forward = newForward;
                Up = newUp;
                initialLife = 0;
                lifeSpan = 0;

                PColor = Color.White;
                Speed = 0;
                Size = 1;
                Gravity = 0;
                currentGravity = 0;
            }

            // ***** ACCESSORS *****

            public bool IsActive { get { return (lifeSpan > 0) ? true : false; } }

            public float LifePercent { get { return lifeSpan / initialLife; } }

            public Matrix World { get { return Matrix.CreateScale(Size) * Matrix.CreateWorld(Position, Forward, Up); } }

            // ***** USER METHODS *****

            public void Emit(Vector3 newPos, Vector3 newForward, Vector3 newUp, float newLifespan, float newSpeed, float newSize, float newGravity)
            {
                Position = newPos;
                Forward = newForward;
                Up = newUp;
                initialLife = newLifespan;
                lifeSpan = newLifespan;
                PColor = Color.White;
                Size = newSize;
                Speed = newSpeed;
                Gravity = newGravity;
                currentGravity = 0;
            }

            public bool Update(float tSeconds)
            {
                bool returnActive = false;
                if (lifeSpan > 0)
                {
                    currentGravity += Gravity * tSeconds;
                    Position += (Speed * Forward - currentGravity * Up) * tSeconds;
                    lifeSpan -= tSeconds;
                    returnActive = true;
                }
                return returnActive;
            }
        }

        static VertexDeclaration InstanceVD = new VertexDeclaration
        (
            new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 0),
            new VertexElement(16, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 1),
            new VertexElement(32, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 2),
            new VertexElement(48, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 3),
            new VertexElement(64, VertexElementFormat.Color, VertexElementUsage.Color, 0)
        );
    }
}
